今天,我們要來實作健身房管理系統中另一個重要的功能:課程管理。我們來建立課程列表頁面、課程詳情頁面,以及一個日曆來顯示課程安排。
首先,我們需要安裝一些與日曆和表單處理相關的套件:
npm install react-big-calendar date-fns
npm install -D @types/react-big-calendar
我們將首先建立課程列表頁面,並利用 TanStack Table 來提供排序和分頁功能,以提升列表的易讀性和可操作性。
在 src/pages/Classes.tsx
中:
import React from 'react';
import { useQuery } from '@tanstack/react-query';
import { useTable, useSortBy, usePagination } from '@tanstack/react-table';
import axios from 'axios';
import { Button } from "@/components/ui/button"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
interface Class {
id: number;
name: string;
instructor: string;
date: string;
time: string;
duration: number;
}
const fetchClasses = async (): Promise<Class[]> => {
const { data } = await axios.get<Class[]>('/api/classes');
return data;
};
const Classes: React.FC = () => {
const { data: classes, isLoading, error } = useQuery(['classes'], fetchClasses);
const columns = React.useMemo(
() => [
{
Header: '課程名稱',
accessor: 'name',
},
{
Header: '教練',
accessor: 'instructor',
},
{
Header: '日期',
accessor: 'date',
},
{
Header: '時間',
accessor: 'time',
},
{
Header: '時長 (分鐘)',
accessor: 'duration',
},
],
[]
);
const {
getTableProps,
getTableBodyProps,
headerGroups,
page,
nextPage,
previousPage,
canNextPage,
canPreviousPage,
prepareRow,
} = useTable(
{ columns, data: classes || [] },
useSortBy,
usePagination
);
if (isLoading) return <div>載入中...</div>;
if (error) return <div>發生錯誤:{(error as Error).message}</div>;
return (
<div>
<h1 className="text-2xl font-bold mb-4">課程列表</h1>
<Table {...getTableProps()}>
<TableHeader>
{headerGroups.map(headerGroup => (
<TableRow {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<TableHead {...column.getHeaderProps(column.getSortByToggleProps())}>
{column.render('Header')}
<span>
{column.isSorted
? column.isSortedDesc
? ' 🔽'
: ' 🔼'
: ''}
</span>
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody {...getTableBodyProps()}>
{page.map(row => {
prepareRow(row);
return (
<TableRow {...row.getRowProps()}>
{row.cells.map(cell => (
<TableCell {...cell.getCellProps()}>{cell.render('Cell')}</TableCell>
))}
</TableRow>
);
})}
</TableBody>
</Table>
<div className="mt-4">
<Button onClick={() => previousPage()} disabled={!canPreviousPage}>
上一頁
</Button>
<Button onClick={() => nextPage()} disabled={!canNextPage}>
下一頁
</Button>
</div>
</div>
);
};
export default Classes;
接著,我們將建立一個日曆視圖,讓管理員能夠直觀地查看每周或每月的課程安排。我們將使用 React Big Calendar 來完成這個功能。
在 src/pages/ClassCalendar.tsx
中:
import React from 'react';
import { Calendar, momentLocalizer } from 'react-big-calendar';
import moment from 'moment';
import 'react-big-calendar/lib/css/react-big-calendar.css';
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
moment.locale('zh-TW');
const localizer = momentLocalizer(moment);
interface Class {
id: number;
title: string;
start: Date;
end: Date;
instructor: string;
}
const fetchClasses = async (): Promise<Class[]> => {
const { data } = await axios.get<Class[]>('/api/classes');
return data.map(cls => ({
...cls,
start: new Date(cls.start),
end: new Date(cls.end),
}));
};
const ClassCalendar: React.FC = () => {
const { data: classes, isLoading, error } = useQuery(['classes'], fetchClasses);
if (isLoading) return <div>載入中...</div>;
if (error) return <div>發生錯誤:{(error as Error).message}</div>;
return (
<div style={{ height: '500px' }}>
<Calendar
localizer={localizer}
events={classes}
startAccessor="start"
endAccessor="end"
style={{ height: '100%' }}
messages={{
next: "下一個",
previous: "上一個",
today: "今天",
month: "月",
week: "週",
day: "日"
}}
/>
</div>
);
};
export default ClassCalendar;
接下來,我們將建立一個課程詳情和編輯頁面,讓管理員可以查看和修改課程資訊。
在 src/pages/ClassDetail.tsx
中:
import React from 'react';
import { useParams } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useForm } from 'react-hook-form';
import axios from 'axios';
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardHeader, CardTitle, CardContent, CardFooter } from "@/components/ui/card"
interface Class {
id: number;
name: string;
instructor: string;
date: string;
time: string;
duration: number;
}
const fetchClass = async (id: string): Promise<Class> => {
const { data } = await axios.get<Class>(`/api/classes/${id}`);
return data;
};
const updateClass = async (classData: Class): Promise<Class> => {
const { data } = await axios.put<Class>(`/api/classes/${classData.id}`, classData);
return data;
};
const ClassDetail: React.FC = () => {
const { id } = useParams<{ id: string }>();
const queryClient = useQueryClient();
const { data: classData, isLoading, error } = useQuery(['class', id], () => fetchClass(id!));
const { register, handleSubmit, formState: { errors } } = useForm<Class>();
const mutation = useMutation(updateClass, {
onSuccess: () => {
queryClient.invalidateQueries(['class', id]);
},
});
const onSubmit = (data: Class) => {
mutation.mutate(data);
};
if (isLoading) return <div>載入中...</div>;
if (error) return <div>發生錯誤:{(error as Error).message}</div>;
return (
<Card>
<CardHeader>
<CardTitle>課程詳情</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="space-y-4">
<Input {...register('name', { required: '請輸入課程名稱' })} defaultValue={classData?.name} placeholder="課程名稱" />
{errors.name && <p className="text-red-500">{errors.name.message}</p>}
<Input {...register('instructor', { required: '請輸入教練姓名' })} defaultValue={classData?.instructor} placeholder="教練" />
{errors.instructor && <p className="text-red-500">{errors.instructor.message}</p>}
<Input {...register('date')} defaultValue={classData?.date} placeholder="日期" type="date" />
<Input {...register('time')} defaultValue={classData?.time} placeholder="時間" type="time" />
<Input {...register('duration', { required: '請輸入課程時長', min: 1 })} defaultValue={classData?.duration} placeholder="時長 (分鐘)" type="number" />
{errors.duration && <p className="text-red-500">{errors.duration.message}</p>}
</div>
<Button type="submit" className="mt-4">更新課程資料</Button>
</form>
</CardContent>
</Card>
);
};
export default ClassDetail;
最後,我們將這些新頁面整合進路由系統。在 src/App.tsx
中進行相應更新:
import React from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import MainLayout from './layouts/MainLayout';
import Dashboard from './pages/Dashboard';
import Members from './pages/Members';
import MemberDetail from './pages/MemberDetail';
import Classes from './pages/Classes';
import ClassCalendar from './pages/ClassCalendar';
import ClassDetail from './pages/ClassDetail';
import Login from './pages/Login';
function App() {
return (
<Router>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/" element={<MainLayout />}>
<Route index element={<Dashboard />} />
<Route path="members" element={<Members />} />
<Route path="members/:id" element={<MemberDetail />} />
<Route path="classes" element={<Classes />} />
<Route path="class-calendar" element={<ClassCalendar />} />
<Route path="classes/:id" element={<ClassDetail />} />
</Route>
</Routes>
</Router>
);
}
export default App;
今天完成了 Gym Pro 系統中的課程管理功能,包括:
接下來,我們將繼續開發更多實用功能,提升 Gym Pro 的可用性與用戶體驗。